Skip to content

Hubspot certificate object#3604

Merged
cp-at-mit merged 21 commits into
mainfrom
hubspot-certificate-object
Jun 2, 2026
Merged

Hubspot certificate object#3604
cp-at-mit merged 21 commits into
mainfrom
hubspot-certificate-object

Conversation

@cp-at-mit

@cp-at-mit cp-at-mit commented May 22, 2026

Copy link
Copy Markdown
Contributor

What are the relevant tickets?

https://github.com/mitodl/hq/issues/11345

Description (What does it do?)

Changes the way we store certificate information in Hubspot. Currently we store this information in a multi-checkbox property on the Contact object in Hubspot. This PR changes that so now we create a custom Hubspot object for certificates, create a new object for each user's certificate, and then link the certificate objects to the contact object in Hubspot.

How can this be tested?

Run the new management command to ensure that the customer contract object is created in the Hubspot account you're using (docker compose exec web python manage.py create_hubspot_certificate_schema)

You then can create a new course run or program certificate for your user through Django admin. Once you save the new certificate, you should see the certificate linked to your contact in Hubspot. To see them in Hubspot, you may need to go to "Data Management" -> "Data Model". There you should see the custom objects.

cp-at-mit added 4 commits May 19, 2026 13:05
Switch certificate syncing from contact properties to dedicated HubSpot custom objects. Adds API helpers to upsert course-run and program certificate objects (unique_app_id handling, association helpers, and schema defs), Celery tasks to enqueue certificate syncs, a management command to create/inspect certificate schemas and print objectType/association IDs, and signal handlers to enqueue tasks on certificate creation. Removes certificate fields from the contact serializer and updates contact sync logic (skip_certificates is now a deprecated no-op). Adds settings for object types and association IDs and updates/extends tests accordingly.
@github-actions

Copy link
Copy Markdown

OpenAPI Changes

Show/hide ## Changes for v0.yaml:
## Changes for v0.yaml:
No changes detected

## Changes for v1.yaml:
No changes detected

## Changes for v2.yaml:
No changes detected

Unexpected changes? Ensure your branch is up-to-date with main (consider rebasing).

Comment thread hubspot_sync/api.py Fixed
Comment thread hubspot_sync/api.py Dismissed
@cp-at-mit cp-at-mit marked this pull request as ready for review May 26, 2026 18:56
@annagav annagav self-requested a review May 28, 2026 11:30

@dsubak dsubak left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks mostly good - I left a few comments/questions, but nothing that should be considered blocking.

Regarding testing - has create_hubspot_certificate_schema been run in the sandbox? I was poking around to see if I could verify the new object structure, but wasn't able to find them. I'm configured against the sandbox, if you'd like me to try and validate the functionality against mitxonlinedev-2026

Comment thread courses/signals.py Outdated
Comment on lines +55 to +64
try:
transaction.on_commit(
lambda: hubspot_tasks.sync_course_run_certificate_with_hubspot.delay(
instance.id
)
)
except Exception: # pylint: disable=broad-except
logger = logging.getLogger(__name__)
logger.exception("Error syncing HubSpot course run certificate")
# avoid blocking certificate save flow

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this try/except make sense to retain anymore? Previously we were doing some actual work in the upsert_custom_properties method and then tasking out in sync_hubspot_user. If I'm reading this correctly, now we're only catching exceptions that could come out of Django when registering that callback?

Comment thread courses/signals.py Outdated
):
"""When a ProgramCertificate model is created."""
_ = created
try:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question here w/r/t the try/except

Comment thread hubspot_sync/api.py
"user_email",
],
"associatedObjects": ["CONTACT"],
"properties": [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dealer's choice as to whether or not you want to do this - do you think it'd be useful to pull out the common properties and reference them here to reduce some boilerplate? It looks like user_email, issue_date, unique_app_id and is_revoked are all defined the same way - we could define those as constants and reference them in both places if we think they're always gonna be consistent.

Comment thread hubspot_sync/api.py Outdated
"""Create a v4 association between a certificate custom object and a contact.

Uses create_default() which auto-creates associations with the default type.
The association_type_id parameter is kept for backward compatibility but not used.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies for the dumb question, but where is association_type_id needed for backwards compatibility? This is net new code right?

I see we're looking it up from settings or the API to pass into this method but I'm not clear on why we need it.

Comment thread hubspot_sync/api.py Outdated

wait_for_hubspot_rate_limit()
if existing_id:
result = hubspot_client.crm.objects.basic_api.update(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe hubspot_certificate_result? Just to make it a bit more clear what this response corresponds to?

Comment thread hubspot_sync/serializers.py
@cp-at-mit

Copy link
Copy Markdown
Contributor Author

Looks mostly good - I left a few comments/questions, but nothing that should be considered blocking.

Regarding testing - has create_hubspot_certificate_schema been run in the sandbox? I was poking around to see if I could verify the new object structure, but wasn't able to find them. I'm configured against the sandbox, if you'd like me to try and validate the functionality against mitxonlinedev-2026

I was using one of the older test environments. I think you're good to run this command against that environment.

cp-at-mit and others added 5 commits June 2, 2026 09:04
Remove broad exception handling in certificate signal handlers and always schedule HubSpot sync tasks via transaction.on_commit. Consolidate certificate custom-object property definitions by introducing CERTIFICATE_COMMON_PROPERTIES and CERTIFICATE_IS_REVOKED_OPTIONS and reuse them in course_run and program certificate schemas to reduce duplication. Remove unused _get_cert_contact_association_type_id and update sync functions to use the resolved custom object type and hubspot result variable (hubspot_certificate_result) when creating/updating and associating certificates. Update tests to use SimpleNamespace-based certificate fixtures, mock _get_custom_object_type_id_by_name, and adjust assertions to match the new association and object-type usage.
@dsubak

dsubak commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

@cp-at-mit Looks good in mitxonlinedev-2026!

Screenshot 2026-06-02 at 12 49 56 PM
root@a592b1a7d47e:/src# ./manage.py create_hubspot_certificate_schema
/opt/venv/lib/python3.11/site-packages/mitol/__init__.py:1: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  __import__("pkg_resources").declare_namespace(__name__)
Static file directory was missing: /src/frontend/staff-dashboard/build
2026-06-02T16:48:37.640319Z [info     ] Initializing OpenTelemetry     [mitol.observability.telemetry]
2026-06-02T16:48:37.672163Z [info     ] OpenTelemetry initialized successfully [mitol.observability.telemetry]
Created schema course_run_certificate (objectTypeId=2-63527265)
Created schema program_certificate (objectTypeId=2-63527267)

Certificate schema details:
- course_run_certificate
  objectTypeId: 2-63527265
  contact association typeId: 17
- program_certificate
  objectTypeId: 2-63527267
  contact association typeId: 35

Add/update these environment settings:
HUBSPOT_COURSE_RUN_CERTIFICATE_OBJECT_TYPE=course_run_certificate
HUBSPOT_COURSE_RUN_CERTIFICATE_ASSOCIATION_TYPE_ID=17
HUBSPOT_PROGRAM_CERTIFICATE_OBJECT_TYPE=program_certificate
HUBSPOT_PROGRAM_CERTIFICATE_ASSOCIATION_TYPE_ID=35

@annagav annagav left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was able to verify the creation of certificate objects 👍

@cp-at-mit cp-at-mit merged commit de3f8e8 into main Jun 2, 2026
9 checks passed
@cp-at-mit cp-at-mit deleted the hubspot-certificate-object branch June 2, 2026 19:00
@odlbot odlbot mentioned this pull request Jun 2, 2026
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants